Hybrid Deep Linking Documentation

Technical strategy for handling task links sent via email, supporting both app and web access.

1. The Goal & Link Structure

The primary goal is to use a **single, standard HTTPS link** in all email communications. This link must intelligently handle users on all platforms (Mobile/Desktop) and scenarios (App Installed/App Not Installed, Web Access/No Web Access).

Link Structure

The link will be a standard web link to our domain. It must not be a custom URL scheme (e.g., myapp://).

https://www.gotransparent.com/transparent/projects/tasks/view/2234827

Where 2234827 is the dynamic task ID.

2. User Click Flow

This flow chart visualizes what happens when a user clicks the link.

User clicks: https://www.gotransparent.com/transparent/projects/tasks/view/2234827

On Mobile (iOS or Android)

OS checks for associated app
App Installed? YES

OS intercepts the link. App opens directly to Task 2234827.

App Installed? NO

Link opens in browser. Page redirects to App/Play Store.

On Desktop (Laptop)

Link opens in browser. Page checks user auth/permission.
Web Access? YES

Page loads and displays the task content as usual on the web.

Web Access? NO

Page displays a message: "Please view this task on our mobile app" with download links.

3. Mobile Team Tasks (App-Side)

The mobile team must configure the app to associate with the domain and handle the incoming URL.

iOS (Universal Links)

  • Enable Associated Domains: In Xcode, under "Signing & Capabilities", add the "Associated Domains" capability.
  • Add Domain: Add applinks:www.gotransparent.com to the domains list.
  • Handle URL: In your AppDelegate or SceneDelegate, implement the application(_:continue:restorationHandler:) method to receive the URL, parse the task ID, and navigate to the correct screen.
// Example in SceneDelegate.swift
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
          let incomingURL = userActivity.webpageURL,
          let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true) else {
        return
    }

    // Example path: /transparent/projects/tasks/view/2234827
    if components.path.starts(with: "/transparent/projects/tasks/view/") {
        let taskID = components.path.replacingOccurrences(of: "/transparent/projects/tasks/view/", with: "")
        print("Received task ID: \(taskID)")
        // Add your navigation logic here
        // e.g., rootViewController.navigateToTask(with: taskID)
    }
}

Android (App Links)

  • Add Intent Filter: In AndroidManifest.xml, add an intent filter to the relevant Activity.
  • Set android:autoVerify="true" to enable App Link verification.
  • Handle Intent: In your Activity's onCreate or onNewIntent method, get the data from the intent, parse the URL, and navigate.
<!-- Example in AndroidManifest.xml -->
<activity ...>
    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data
            android:scheme="https"
            android:host="www.gotransparent.com"
            android:pathPrefix="/transparent/projects/tasks/view" />
    </intent-filter>
</activity>

4. Web Team Tasks (Server-Side)

The web team has two responsibilities: 1) Host the verification files, and 2) Build the fallback/redirect page with permission checks.

Part A: Domain Verification Files

These files must be hosted at the specified paths to prove to Apple and Google that we own the domain.

iOS Verification File

  • Path: https://www.gotransparent.com/.well-known/apple-app-site-association
  • Note: The file has *no* .json extension.
{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "YOUR_TEAM_ID.com.yourcompany.appbundleid",
        "paths": [ "/transparent/projects/tasks/view/*" ]
      }
    ]
  }
}

Android Verification File

  • Path: https://www.gotransparent.com/.well-known/assetlinks.json
[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.yourcompany.apppackage",
    "sha256_cert_fingerprints": [
      "YOUR:SHA256:CERT:FINGERPRINT:GOES:HERE"
    ]
  }
}]

Part B: Fallback Logic Page

This is the page that loads at https://www.gotransparent.com/transparent/projects/tasks/view/. It detects the user's device and, if on desktop, checks permissions.

Here is a complete, simple HTML file that can serve as this page.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>View Task</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        body { font-family: 'Inter', sans-serif; }
    </style>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
    
    <script>
        (function() {
            // --- CONFIGURE THESE ---
            const APP_STORE_URL = "https://apps.apple.com/us/app/your-app/id123456789";
            const PLAY_STORE_URL = "https://play.google.com/store/apps/details?id=com.yourcompany.apppackage";
            // ---------------------

            const ua = navigator.userAgent || navigator.vendor || window.opera;

            // Check for iOS
            if (/iPad|iPhone|iPod/.test(ua) && !window.MSStream) {
                // On mobile (iOS) and app is not installed, always redirect to store.
                window.location.replace(APP_STORE_URL);
                return;
            }

            // Check for Android
            if (/android/i.test(ua)) {
                // On mobile (Android) and app is not installed, always redirect to store.
                window.location.replace(PLAY_STORE_URL);
                return;
            }
            
            // If neither, it's a desktop.
            // Run desktop logic after the page loads.
            document.addEventListener("DOMContentLoaded", function() {
                
                // --- PLACEHOLDER: This is where you check user's web permission ---
                // This function should return true if user can view tasks on web, false otherwise.
                // You might check for a session cookie, make an API call, etc.
                // This is an ASYNCHRONOUS example (e.g., fetching from an API).
                async function checkUserWebPermission() {
                    console.log("Checking user web permissions...");
                    // Example: Check for a hypothetical session cookie
                    // const hasSession = document.cookie.includes('session_id=');
                    // if (hasSession) return true;

                    // Example: Make a fake API call
                    // try {
                    //   const response = await fetch('/api/v1/me/permissions');
                    //   const data = await response.json();
                    //   return data.canViewWebTasks === true;
                    // } catch (e) {
                    //   return false;
                    // }

                    // For this documentation, we'll default to 'false' to show the message.
                    // Replace this with your real logic.
                    return new Promise(resolve => {
                        setTimeout(() => resolve(false), 500); // Simulating API call
                    });
                }
                // -----------------------------------------------------------------

                
                checkUserWebPermission().then(hasPermission => {
                    if (hasPermission) {
                        // User IS authorized. Show the task content.
                        // You would replace this with your app's logic to load the task.
                        document.getElementById("loader").style.display = "none";
                        document.getElementById("task-content-placeholder").style.display = "block";
                    } else {
                        // User is NOT authorized. Show the download message.
                        document.getElementById("loader").style.display = "none";
                        document.getElementById("desktop-message").style.display = "flex";
                        document.getElementById("app-store-link").href = APP_STORE_URL;
                        document.getElementById("play-store-link").href = PLAY_STORE_URL;
                    }
                });
            });
        })();
    </script>
</head>
<body class="bg-gray-100 flex items-center justify-center min-h-screen">

    <!-- Loader (shown while script runs) -->
    <div id="loader" class="text-center">
        <p class="text-gray-600 text-lg">Loading task...</p>
    </div>

    <!-- Desktop Message (hidden by default) -->
    <div id="desktop-message" style="display: none;" class="flex-col items-center bg-white p-8 sm:p-12 rounded-lg shadow-lg max-w-lg text-center">
        <svg class="w-16 h-16 text-blue-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"></path></svg>
        <h1 class="text-2xl sm:text-3xl font-bold text-gray-800 mb-4">This task is available on mobile only.</h1>
        <p class="text-gray-600 text-lg mb-6">Please open this link on your phone to view the task, or download our app below.</p>
        <div class="flex flex-col sm:flex-row gap-4">
            <a id="app-store-link" href="#" class="bg-gray-900 text-white font-semibold py-3 px-6 rounded-lg shadow hover:bg-gray-700 transition">
                Download on the App Store
            </a>
            <a id="play-store-link" href="#" class="bg-gray-900 text-white font-semibold py-3 px-6 rounded-lg shadow hover:bg-gray-700 transition">
                Get it on Google Play
            </a>
        </div>
    </div>

    <!-- Task Content Placeholder (hidden by default) -->
    <div id="task-content-placeholder" style="display: none;" class="bg-white p-8 sm:p-12 rounded-lg shadow-lg max-w-4xl text-left">
        

Task 2234827 (Example)

<p class="text-gray-700 text-lg">User is authorized.</p> <p class="text-gray-600 mt-2">Your full web application or task content would be loaded here.</p> <!-- Your web app's content goes here --> </div> </body> </html>

5. Alternative: Third-Party Services

If managing the verification files and redirect logic becomes too complex, services like Branch.io or AppsFlyer specialize in this.

  • They provide a single "smart link" that handles all device detection and redirection.
  • They also provide "deferred deep linking" (remembering the link *through* the app install).
  • This is a good option if our in-house solution becomes difficult to maintain.